﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Security.Cryptography;
    using System.Security; 
    #endregion

    public class RandomPasswordGenerator {

        #region Enums
        [Flags]
        public enum PasswordCharacterOptions {
            Undefined = 0,
            Alpha = 0x00000001,
            Numeric = 0x00000002,
            Special = 0x00000004,
            AlphaNumeric = Alpha | Numeric,
            AlphaNumericSpecial = Alpha | Numeric | Special
        } 
        #endregion

        #region Members
        private const int DEFAULT_PASSWORD_LENGTH = 24;

        // Disallow characters that are confusing: i, I, l, L, o, O, 0, 1, u, v  
        private static readonly string PASSWORD_CHARS_ALPHA = "abcdefghjkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ";
        private static readonly string PASSWORD_CHARS_NUMERIC = "23456789";
        private static readonly string PASSWORD_CHARS_SPECIAL = "!@#$%&*-=+{}?";
        private static readonly Dictionary<PasswordCharacterOptions, string> PasswordCharacters;

        private static readonly int minPasswordLength = 7;
        // http://technet.microsoft.com/en-us/library/bb726984.aspx
        private static readonly int maxPasswordLength = 104;
        #endregion

        #region Constructor
        static RandomPasswordGenerator() {
            PasswordCharacters = new Dictionary<PasswordCharacterOptions, string>();
            PasswordCharacters.Add(PasswordCharacterOptions.Alpha, PASSWORD_CHARS_ALPHA);
            PasswordCharacters.Add(PasswordCharacterOptions.Numeric, PASSWORD_CHARS_NUMERIC);
            PasswordCharacters.Add(PasswordCharacterOptions.AlphaNumeric, PASSWORD_CHARS_ALPHA + PASSWORD_CHARS_NUMERIC);
            PasswordCharacters.Add(PasswordCharacterOptions.AlphaNumericSpecial, PASSWORD_CHARS_ALPHA + PASSWORD_CHARS_NUMERIC + PASSWORD_CHARS_SPECIAL);
        } 
        #endregion

        /// <summary>  
        /// Generates the password
        /// </summary>  
        public static SecureString GeneratePassword(
            int passwordLength = DEFAULT_PASSWORD_LENGTH, 
            PasswordCharacterOptions passwordCharacterOptions = PasswordCharacterOptions.AlphaNumericSpecial, 
            bool noConsecutive = false) {

            #region Validation
            if (passwordCharacterOptions == PasswordCharacterOptions.Undefined) {
                throw new ArgumentOutOfRangeException("passwordCharacterOptions", "PasswordCharacterOptions.Undefined is invalid.");
            } 
            #endregion

            if (passwordLength < minPasswordLength) passwordLength = minPasswordLength;
            if (passwordLength > maxPasswordLength) passwordLength = maxPasswordLength;

            char[] passwordCharacters = null;

            var random = new Random(Guid.NewGuid().GetHashCode());

            char passwordChar = char.MaxValue;
            char previousPasswordChar = char.MaxValue;

            int attempts = 0;
            do {
                passwordCharacters = new char[passwordLength];
                for (int passwordLengthIndex = 0; passwordLengthIndex < passwordLength; passwordLengthIndex++) {
                    do {
                        int passwordCharacterIndex = random.Next(PasswordCharacters[passwordCharacterOptions].Length);
                        passwordChar = PasswordCharacters[passwordCharacterOptions][passwordCharacterIndex];
                    } while (noConsecutive && (previousPasswordChar == passwordChar));

                    previousPasswordChar = passwordChar;
                    passwordCharacters[passwordLengthIndex] = passwordChar;
                }
            } while ((VerifyPasswordComplexity(passwordCharacters, passwordCharacterOptions) == false) && (attempts++ < 100));

            var securePassword = new SecureString();
            foreach (var passwordCharacter in passwordCharacters) {
                securePassword.AppendChar(passwordCharacter);
            }
            securePassword.MakeReadOnly();
            passwordCharacters = null;

            return securePassword;
        }

        /// <summary>
        /// For the PasswordCharacterOptions.AlphaNumeric and PasswordCharacterOptions.AlphaNumericSpecial, verifies
        /// that at least one character is from each specified set.
        /// </summary>
        private static bool VerifyPasswordComplexity(char[] passwordCharacters, PasswordCharacterOptions passwordCharacterOptions) {
            bool meetsComplexityRequirements = true;

            if (passwordCharacterOptions.HasFlag(PasswordCharacterOptions.Alpha)) {
                if (!passwordCharacters.Any(x => PASSWORD_CHARS_ALPHA.Any(y => y == x))) {
                    meetsComplexityRequirements = false;
                }
            }

            if (passwordCharacterOptions.HasFlag(PasswordCharacterOptions.Numeric)) {
                if (!passwordCharacters.Any(x => PASSWORD_CHARS_NUMERIC.Any(y => y == x))) {
                    meetsComplexityRequirements = false;
                }
            }

            if (passwordCharacterOptions.HasFlag(PasswordCharacterOptions.Special)) {
                if (!passwordCharacters.Any(x => PASSWORD_CHARS_SPECIAL.Any(y => y == x))) {
                    meetsComplexityRequirements = false;
                }
            }

            return meetsComplexityRequirements;
        }
    }
}
